<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="assets/css/bootstrap.min.css">
    <link rel="stylesheet" href="assets/css/main.css">
    <link rel="stylesheet" href="assets/css/xterm.css">
    <link rel="stylesheet" href="assets/css/bootstrap-icons.min.css">
    <script src="assets/js/bootstrap.bundle.min.js" type="application/javascript"></script>
    <script src="assets/js/vue.min.js" type="application/javascript"></script>
    <script src="assets/js/xterm.js" type="application/javascript"></script>
    <script src="assets/js/xterm-addon-fit.js" type="application/javascript"></script>
    <title>Zero Terminal</title>
</head>

<body>

    <div id="app">
        <div v-if="connectionList.length > 0" class="zt-main">
            <div class="row">
                <!-- 连接列表 -->
                <div class="col-md-3" style="height: 95vh; overflow: auto">
                    <div><button class="btn btn-primary" @click="createConnection" style="width: 100%; margin: 15px 0">创建连接</button></div>
                    <div>
                        <button v-if="core.activeLeftNav === 'conn'" class="btn btn-primary" style="width: 48%" @click="changeLeftNav('conn')">连接管理</button>
                        <button v-else class="btn btn-outline-primary" style="width: 48%" @click="changeLeftNav('conn')">连接管理</button>
                        <button v-if="core.activeLeftNav === 'file'" class="btn btn-primary" style="width: 48%; float: right" @click="changeLeftNav('file')">文件管理</button>
                        <button v-else class="btn btn-outline-primary" style="width: 48%; float: right" @click="changeLeftNav('file')">文件管理</button>
                    </div>
                    <div v-if="core.activeLeftNav === 'conn'">
                        <div class="zt-conn-main" v-for="conn in connectionList">
                            <span style="cursor: pointer;">{{ conn.name }}</span>
                            <span style="color: #0d6efd; margin-left: 10px; cursor: pointer;"
                                  @click="editConnection(conn)">编辑</span>
                            <span style="color: red;  margin-left: 10px; cursor: pointer;"
                                  @click="confirmDeleteConnection(conn)">删除</span>
                            <span style="color: #0d6efd; margin-left: 10px; cursor: pointer;"
                                  @click="initTerminal(conn)">连接</span>
                        </div>
                    </div>
                    <div v-else>
                        <div class="input-group flex-nowrap" style="padding: 10px 0">
                            <span class="input-group-text" id="addon-wrapping">当前路径</span>
                            <input type="text" class="form-control" placeholder="filepath" aria-label="filepath"
                                   aria-describedby="addon-wrapping" v-model="core.filepathMap[core.activeId]" @change="changeFilepath()">
                        </div>
                        <div>
                            <button class="btn btn-primary" @click="fileUpload">文件上传</button>
                        </div>
                        <div v-for="file in files">
                            <div @click="fileClick(file)">
                                <i v-if="file.attrs.mode === 16877 || file.attrs.mode === 16893" class="bi-folder" style="font-size: 1rem; color: cornflowerblue;"></i>
                                <span>{{ file.filename }}</span>
                            </div>
                        </div>
                    </div>
                </div>
                <!-- ssh 终端 -->
                <div id="terminal-container" class="col-md-9">
                    <ul class="nav nav-pills" style="margin: 10px 0;">
                        <li class="nav-item" v-for="(tab, index) in core.tabs" @click="showTerm(tab)">
                            <a class="nav-link" href="#" :class="{ 'active': tab.termId === core.activeId}">
                                {{ tab.name }}
                                <span class="btn btn-danger" @click.stop="deleteTerm(tab, index)">删除</span>
                            </a>
                        </li>
                    </ul>
                    <div v-for="term in core.terms">
                        <div :id="term.termId" v-show="term.termId === core.activeId" style="height: 90vh;"></div>
                    </div>
                </div>
            </div>
        </div>
        <div v-else class="card center" style="width: 50%;">
            <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#saveSshConnection"
                @click="createConnection">
                创建 SSH 连接
            </button>
        </div>

        <!-- 弹出框, 创建/编辑 SSH 连接 -->
        <div class="modal modal-lg fade" id="saveSshConnection" tabindex="-1" aria-labelledby="saveSshConnectionLabel"
            aria-hidden="true">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h1 class="modal-title fs-5" id="saveSshConnectionLabel">创建/编辑 SSH连接</h1>
                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                    </div>
                    <div class="modal-body">
                        <div class="input-group mt-2">
                            <label class="input-group-text" id="label-name" for="name">连接名称</label>
                            <input v-model="connection.name" type="text" class="form-control" id="name"
                                placeholder="连接名称默认为连接地址">
                        </div>
                        <div class="d-flex mt-2">
                            <div class="input-group">
                                <label class="input-group-text" id="label-address" for="address">连接地址</label>
                                <input v-model="connection.address" type="text" class="form-control" id="address"
                                    placeholder="请输入连接地址">
                            </div>
                            <div class="input-group">
                                <label class="input-group-text" id="label-port" for="port">端口</label>
                                <input v-model="connection.port" type="text" class="form-control" id="port"
                                    placeholder="端口号默认为 22">
                            </div>
                        </div>
                        <div class="input-group mt-2">
                            <label class="input-group-text" id="label-username" for="username">用户名称</label>
                            <input v-model="connection.username" type="text" class="form-control" id="username"
                                placeholder="请输入用户名">
                        </div>
                        <div class="input-group mt-2">
                            <label class="input-group-text" id="label-password" for="password">用户密码</label>
                            <input v-model="connection.password" type="password" class="form-control" id="password"
                                placeholder="请输入密码">
                        </div>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
                        <button type="button" class="btn btn-primary" data-bs-dismiss="modal"
                            @click="saveConnection">保存</button>
                    </div>
                </div>
            </div>
        </div>

        <!-- 弹出框, 确认删除 -->
        <div class="modal fade" id="deleteSshConnection" tabindex="-1" aria-labelledby="deleteSshConnectionLabel"
            aria-hidden="true">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h1 class="modal-title fs-5" id="deleteSshConnectionLabel">确认删除么?</h1>
                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
                        <button type="button" class="btn btn-primary" data-bs-dismiss="modal"
                            @click="deleteConnection">确认</button>
                    </div>
                </div>
            </div>
        </div>

        <!-- 提示框 -->
        <div class="toast-container position-fixed bottom-0 end-0 p-3">
            <div id="liveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
                <div class="toast-header">
                    <strong class="me-auto">ZeroTerminal</strong>
                    <small>{{ toast.subTitle }}</small>
                    <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
                </div>
                <div class="toast-body" v-html="toast.content">
                </div>
            </div>
        </div>
    </div>

    <script>
        const { ipcRenderer, shell } = require('electron');
        const { Client } = require('ssh2');
        const fs = require('fs');
        const path = require('path');

        new Vue({
            el: '#app',
            data: {
                connectionList: [],
                connection: {
                    identity: "",
                    name: "",
                    address: "",
                    port: "22",
                    username: "",
                    password: "",
                },
                core: {
                    index: 0,
                    activeId: "",
                    tabs: [],
                    terms: [],
                    connMap: {}, // 激活的ID => conn
                    activeLeftNav: "conn",
                    filepathMap: {}, // 激活的ID => 文件夹路径
                },
                files: [], // 文件列表
                toast: {
                    subTitle: "",
                    content: ""
                }
            },
            mounted() {
            },
            created() {
                this.initConnections();
            },
            methods: {
                initConnections() {
                    ipcRenderer.invoke('getConnectionList').then((data) => {
                        if (data !== undefined) {
                            this.connectionList = data
                        }
                    });
                },
                initTerminal(connection) { // 初始化终端
                    // 激活ID
                    let activeId = "zero-term" + this.core.index;
                    this.core.activeId = activeId;
                    let newTerm = {
                        name: connection.name,
                        termId: activeId
                    };
                    // 新增导航
                    this.core.tabs.push(newTerm);
                    // 新增term
                    this.core.terms.push(newTerm);
                    // 最大 index 累加
                    this.core.index++;

                    // 在DOM创建后执行的代码
                    this.$nextTick(() => {
                        // 获取终端容器元素
                        // let terminalElem = document.getElementById('terminal-container');
                        let terminalElem = document.getElementById(activeId);

                        // 创建 xterm 终端实例并打开
                        let term = new Terminal();
                        term.open(terminalElem);

                        // 调整终端大小适应容器
                        const fitAddon = new FitAddon.FitAddon();
                        term.loadAddon(fitAddon);
                        fitAddon.fit();

                        let conn = new Client();
                        let channel;

                        // 连接成功时触发
                        conn.on('ready', () => {
                            console.log('SSH 连接已建立');
                            term.write(`Powered by [\x1b[36mZero Terminal\x1b[0m] \r\n \r\n`);

                            // 创建 SSH 通道
                            conn.shell(
                                // 启用伪终端
                                {
                                    term: 'xterm',
                                    rows: term.rows,
                                    cols: term.cols,
                                    usePty: true
                                },
                                (err, ch) => {
                                if (err) {
                                    console.error(err);
                                    term.writeln('创建 SSH 通道失败');
                                    // term.dispose();
                                    return;
                                }

                                channel = ch;

                                // 更新终端大小，以便 SSH 工具适应新的终端宽高
                                channel.setWindow(term.rows, term.cols);

                                // 将通道的数据发送到终端
                                channel.on('data', (data) => {
                                    term.write(data);
                                });

                                // 关闭 SSH 连接时关闭终端和通道
                                conn.on('close', () => {
                                    term.dispose();
                                    channel.close();
                                });
                            });

                            // 保存成功激活的连接
                            this.core.connMap[activeId] = conn;

                            // 将终端的输出写入 SSH 通道
                            term.onData((data) => {
                                if (channel) {
                                    channel.write(data);
                                }
                            });
                        });

                        // 客户端错误时触发
                        conn.on('error', (err) => {
                            console.error('SSH 连接错误:', err);
                            term.write(`Powered by [\x1b[36mZero Terminal\x1b[0m] \r\n \r\n`);
                            term.writeln('SSH 连接错误');
                            // term.dispose();
                        });

                        // 连接 ssh
                        conn.connect({
                            host: connection.address,
                            port: connection.port,
                            username: connection.username,
                            password: connection.password
                        });

                        // 监听终端大小调整的事件
                        window.addEventListener('resize', () => {
                            fitAddon.fit();
                            // 获取调整后的终端宽高
                            const { rows, cols } = term;
                            // 发送宽高信息给 SSH 工具
                            channel.setWindow(rows, cols);
                        });
                    })
                },
                showTerm(tab) {
                    this.core.activeId = tab.termId
                },
                deleteTerm(tab, index) {
                    this.core.tabs.splice(index, 1);
                    // 激活新的 term
                    if (this.core.activeId === tab.termId) {
                        let newIndex = index - 1;
                        if (newIndex === -1) {
                            newIndex = 0;
                        }
                        if (this.core.tabs.length > 0) {
                            this.core.activeId = this.core.tabs[newIndex].termId;
                        }
                    }
                },
                createConnection() { // 创建 SSH 连接时, connection 置空
                    let modal = new bootstrap.Modal(document.getElementById('saveSshConnection'));
                    modal.show();
                    this.connection = {
                        identity: "",
                        name: "",
                        address: "",
                        port: "22",
                        username: "",
                        password: "",
                    }
                },
                saveConnection() {
                    if (this.connection.name === "") {
                        this.connection.name = this.connection.address;
                    }
                    ipcRenderer.invoke('saveConnection', this.connection).then((res) => {
                        console.log(res)
                    }).catch(e => {
                        console.log(e)
                    });
                    this.initConnections();
                },
                confirmDeleteConnection(conn) {
                    this.connection.identity = conn.identity;
                    let modal = new bootstrap.Modal(document.getElementById('deleteSshConnection'));
                    modal.show();
                },
                editConnection(conn) {
                    this.connection = conn
                    let modal = new bootstrap.Modal(document.getElementById('saveSshConnection'));
                    modal.show();
                },
                deleteConnection() {
                    ipcRenderer.invoke('deleteConnection', this.connection).then((res) => {
                        console.log(res)
                    }).catch(e => {
                        console.log(e)
                    });
                    this.initConnections();
                },
                changeLeftNav(type) {
                    this.core.activeLeftNav = type;

                    // 打开文件系统
                    if (type === 'file' && this.core.activeId) {
                        let path = this.core.filepathMap[this.core.activeId] ? this.core.filepathMap[this.core.activeId] : "/";
                        this.sftpReaddir(path);
                    }
                },
                sftpReaddir(path) {
                    let conn = this.core.connMap[this.core.activeId];
                    this.core.filepathMap[this.core.activeId] = path;
                    conn.sftp((err, sftp) => {
                        if (err) console.log(err);
                        sftp.readdir(path, (err, list) => {
                            if (err) console.log(err);

                            // 按文件的名称排序
                            list.sort((a, b) => {
                                return a.filename.localeCompare(b.filename);
                            });

                            this.files = list;
                            console.dir(list);
                        });
                    });
                },
                fileDownload(filename) {
                    let that = this;
                    // 打开文件选择器
                    ipcRenderer.invoke('showSaveDialog', {'filename': filename}).then(localFilepath => {
                        // 下载文件
                        if (localFilepath) {
                            let remoteFilepath = this.core.filepathMap[this.core.activeId] + filename;
                            let conn = this.core.connMap[this.core.activeId];
                            conn.sftp((err, sftp) => {
                                if (err) console.log(err);

                                const readStream = sftp.createReadStream(remoteFilepath);
                                const writeStream = fs.createWriteStream(localFilepath);

                                readStream.pipe(writeStream);

                                readStream.on('close', function() {
                                    console.log('文件下载完成！');
                                    that.showLiveToast({'subTitle': 'now', 'content': '文件:' + filename + ' 下载完成, ' +
                                            `<a onclick='showItemInFolder("${localFilepath.replace(/\\/g, '\\\\')}")' style="color: #0a53be; text-underline: #0a53be; cursor: pointer">打开文件所在位置</a>`});
                                });
                            });
                        }
                    });
                },
                fileClick(file) {
                    // 文件夹双击进入下一层
                    if (file.attrs.mode === 16877 || file.attrs.mode === 16893) {
                        let newFilepath = this.core.filepathMap[this.core.activeId] + file.filename + "/";
                        this.sftpReaddir(newFilepath);
                    } else {
                        this.fileDownload(file.filename);
                        // this.showSaveFileDialog(file.filename);
                    }
                },
                showLiveToast(toastContent) {
                    this.toast = toastContent;
                    const toastLiveExample = document.getElementById('liveToast');
                    const toast = new bootstrap.Toast(toastLiveExample);
                    toast.show();
                },
                changeFilepath() {
                    this.sftpReaddir(this.core.filepathMap[this.core.activeId])
                },
                fileUpload() {
                    let that = this;

                    // 打开文件选择器
                    ipcRenderer.invoke('openFileResourceDialog').then(localFilepath => {
                        // 上传文件
                        if (localFilepath) {
                            let filename = path.basename(localFilepath)
                            let remoteFilepath = this.core.filepathMap[this.core.activeId] + filename;
                            let conn = this.core.connMap[this.core.activeId];
                            conn.sftp((err, sftp) => {
                                if (err) {
                                    console.log(err);
                                    that.showLiveToast({'subTitle': 'now', 'content': '上传失败, ' + err});
                                    return;
                                }
                                sftp.fastPut(localFilepath, remoteFilepath, (err) => {
                                    if (err) {
                                        console.log(err);
                                        that.showLiveToast({'subTitle': 'now', 'content': '上传失败, ' + err});
                                        return;
                                    }
                                    console.log('文件上传成功');
                                    that.showLiveToast({'subTitle': 'now', 'content': '文件:' + filename + ' 上传成功'});
                                });
                            });
                        }
                    });
                }
            }
        });

        function showItemInFolder(path) {
            shell.showItemInFolder(path);
        }
    </script>
</body>

</html>